import * as ansi from 'tui-lib/util/ansi'
import telc from 'tui-lib/util/telchars'

import DisplayElement from './DisplayElement.js'

export default class Root extends DisplayElement {
  // An element to be used as the root of a UI. Handles lots of UI and
  // socket stuff.

  constructor(interfaceArg, writable = null) {
    super()

    this.interface = interfaceArg
    this.writable = writable || interfaceArg

    this.selectedElement = null

    this.cursorBlinkOffset = Date.now()

    this.oldSelectionStates = []

    this.interface.on('inputData', buf => this.handleData(buf))

    this.renderCount = 0
  }

  handleData(buffer) {
    if (telc.isMouse(buffer)) {
      const allData = telc.parseMouse(buffer)
      const { button, line, col } = allData
      const topEl = this.getElementAt(col - 1, line - 1)
      if (topEl) {
        //console.log('Clicked', topEl.constructor.name, 'of', topEl.parent.constructor.name)
        this.eachAncestor(topEl, el => {
          if (typeof el.clicked === 'function') {
            return el.clicked(button, allData) === false
          }
        })
      }
    } else {
      this.eachAncestor(this.selectedElement, el => {
        if (typeof el.keyPressed === 'function') {
          const shouldBreak = (el.keyPressed(buffer) === false)
          if (shouldBreak) {
            return true
          }
          el.emit('keypressed', buffer)
        }
      })
    }
  }

  eachAncestor(topEl, func) {
    // Handy function for doing something to an element and all its ancestors,
    // allowing for the passed function to return false to break the loop and
    // stop propagation.

    if (topEl) {
      const els = [topEl, ...topEl.directAncestors]
      for (const el of els) {
        const shouldBreak = func(el)
        if (shouldBreak) {
          break
        }
      }
    }
  }

  drawTo(writable) {
    writable.write(ansi.moveCursor(0, 0))
    writable.write(' '.repeat(this.w * this.h))
  }

  scheduleRender() {
    if (!this.scheduledRender) {
      setTimeout(() => {
        this.scheduledRender = false
        this.render()
      })
      this.scheduledRender = true
    }
  }

  render() {
    this.renderTo(this.writable)
  }

  renderNow() {
    this.renderNowTo(this.writable)
  }

  renderTo(writable) {
    if (this.anyDescendantShouldRender()) {
      this.renderNowTo(writable)
    }
  }

  renderNowTo(writable) {
    if (writable) {
      this.renderCount++
      super.renderTo(writable)
      // Since shouldRender is false, super.renderTo won't call didRenderTo for
      // us. We need to do that ourselves.
      this.didRenderTo(writable)
    }
  }

  anyDescendantShouldRender() {
    let render = false
    this.eachDescendant(el => {
      // If we already know we're going to render, checking the element's
      // scheduled-draw status (which involves iterating over each of its draw
      // dependency properties) is redundant.
      if (render) {
        return
      }
      render = el.hasScheduledDraw()
    })
    return render
  }

  shouldRender() {
    // We need to return false here because otherwise all children will render,
    // since they'll see the root as an ancestor who needs to be rendered. Bad!
    return false
  }

  didRenderTo(writable) {
    this.eachDescendant(el => {
      el.unscheduleDraw()
      el.updateLastDrawValues()
    })

    /*
    writable.write(ansi.moveCursorRaw(1, 1))
    writable.write('Renders: ' + this.renderCount)
    */

    // Render the cursor, based on the cursorX and cursorY of the currently
    // selected element.
    if (this.selectedElement && this.selectedElement.cursorVisible) {
      /*
      if ((Date.now() - this.cursorBlinkOffset) % 1000 < 500) {
        writable.write(ansi.moveCursor(
          this.selectedElement.absCursorY, this.selectedElement.absCursorX))
        writable.write(ansi.invert())
        writable.write('I')
        writable.write(ansi.resetAttributes())
      }
      */

      writable.write(ansi.showCursor())
      writable.write(ansi.moveCursorRaw(
        this.selectedElement.absCursorY, this.selectedElement.absCursorX))
    } else {
      writable.write(ansi.hideCursor())
    }

    this.emit('rendered')
  }

  cursorMoved() {
    // Resets the blinking animation for the cursor. Call this whenever you
    // move the cursor.

    this.cursorBlinkOffset = Date.now()
  }

  select(el, {fromForm = false} = {}) {
    // Select an element. Calls the unfocus method on the already-selected
    // element, if there is one.

    // If the element is part of a form, just be lazy and pass control to that
    // form...unless the form itself asked us to select the element!
    //
    // TODO: This is so that if an element is selected, its parent form will
    // automatically see that and correctly update its curIndex... but what if
    // the element is an input of a form which is NOT its parent?
    //
    // XXX: We currently use a HUGE HACK instead of `instanceof` to avoid
    // breaking the rule of import direction (controls -> primitives, never
    // the other way around). This is bad for obvious reasons, but I haven't
    // yet looked into what the correct approach would be.
    const parent = el.parent
    if (!fromForm && parent.constructor.name === 'Form' && parent.inputs.includes(el)) {
      parent.selectInput(el)
      return
    }

    const oldSelected = this.selectedElement
    const newSelected = el

    // Relevant elements that COULD have their "isSelected" state change.
    const relevantElements = ([
      ...(oldSelected ? [...oldSelected.directAncestors, oldSelected] : []),
      ...(newSelected ? newSelected.directAncestors : [])
    ]

      // We ignore elements where isSelected is undefined, because they aren't
      // built to handle being selected, and they break the compare-old-and-new-
      // state code below.
      .filter(el => typeof el.isSelected !== 'undefined')

      // Get rid of duplicates - including any that occurred in the already
      // existing array of selection states. (We only care about the oldest
      // selection state, i.e. the one when we did the first .select().)
      .reduce((acc, el) => {
        // Duplicates from relevant elements of current .select()
        if (acc.includes(el)) return acc
        // Duplicates from already existing selection states
        if (this.oldSelectionStates.some(x => x[0] === el)) return acc
        return acc.concat([el])
      }, []))

    // Keep track of whether those elements were selected before we call the
    // newly selected element's selected() function. We store these on a
    // property because we might actually be adding to it from a previous
    // root.select() call, if that one itself caused this root.select().
    // One all root.select()s in the "chain" (as it is) have finished, we'll
    // go through these states and call the appropriate .select/unselect()
    // functions on each element whose .isSelected changed.
    const selectionStates = relevantElements.map(el => [el, el.isSelected])
    this.oldSelectionStates = this.oldSelectionStates.concat(selectionStates)

    this.selectedElement = el

    // Same stuff as in the for loop below. We always call selected() on the
    // passed element, even if it was already selected before.
    if (el.selected) el.selected()
    if (typeof el.focused === 'function') el.focused()

    // If the selection changed as a result of the element's selected()
    // function, stop here. We will leave calling the appropriate functions on
    // the elements in the oldSelectionStates array to the final .select(),
    // i.e. the one which caused no change in selected element.
    if (this.selectedElement !== newSelected) return

    // Compare the old "isSelected" state of every relevant element with their
    // current "isSelected" state, and call the respective selected/unselected
    // functions. (Also call focused and unfocused for some sense of trying to
    // not break old programs, but, like, old programs are going to be broken
    // anyways.)
    const states = this.oldSelectionStates.slice()
    for (const [ el, wasSelected ] of states) {
      // Now that we'll have processed it, we don't want it in the array
      // anymore.
      this.oldSelectionStates.shift()

      const { isSelected } = el
      if (isSelected && !wasSelected) {
        // Don't call these functions if this element is the newly selected
        // one, because we already called them above!
        if (el !== newSelected) {
          if (el.selected) el.selected()
          if (typeof el.focused === 'function') el.focused()
        }
      } else if (wasSelected && !isSelected) {
        if (el.unselected) el.unselected()
        if (typeof el.unfocused === 'function') el.unfocused()
      }

      // If the (un)selected() handler actually selected a different element
      // itself, then further processing of new selected states is irrelevant,
      // so stop here. (We return instead of breaking the for loop because
      // anything after this loop would have already been handled by the call
      // to Root.select() from the (un)selected() handler.)
      if (this.selectedElement !== newSelected) {
        return
      }
    }

    this.cursorMoved()
  }

  isChildOrSelfSelected(el) {
    if (!this.selectedElement) return false
    if (this.selectedElement === el) return true
    if (this.selectedElement.directAncestors.includes(el)) return true
    return false
  }

  get selectedElement() { return this.getDep('selectedElement') }
  set selectedElement(v) { return this.setDep('selectedElement', v) }
}
